{
 "cells": [
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 6.3 卷积神经网络常用结构\n",
    "### 6.3.1 卷积层\n",
    "下图显示了CNN中最重要的部分，这个部分被称为过滤器（filter）或内核（kernel），这里采用TensorFlow文档中的称呼：过滤器。**过滤器可以将当前神经网络结构上的一个子节点矩阵转化为下一层神经网络上的一个单位节点矩阵**，这里单位节点矩阵指的是长宽均为1，深度不限的节点矩阵。\n",
    "<p align='center'>\n",
    "    <img src=images/图6.8.JPG>\n",
    "</p>\n",
    "\n",
    "- 在一个卷积层中，过滤器所处理的节点矩阵的长和宽都是由人工指定的，这个节点矩阵的尺寸也被称之为**过滤器的尺寸**，常用的过滤器尺寸有3×3或5×5。因为过滤器处理的矩阵深度和当前层神经网络节点矩阵的深度是一致的，所以虽然节点矩阵是三维的，但过滤器的尺寸只需要指定两个维度。\n",
    "\n",
    "- 过滤器中另外一个需要人工指定的设置是处理得到的单位节点矩阵的深度，这个设置称为**过滤器的深度**。\n",
    "\n",
    "注意**过滤器的尺寸指的是一个过滤器输入节点矩阵的大小，而深度指的是输出单位节点矩阵的深度**。如图6.8所示，左侧小矩阵的尺寸为过滤器的尺寸，而右侧单位矩阵的深度为过滤器的深度。6.4节将通过一些经典卷积神经网络结构来了解如何设置每一层卷积层过滤器的尺寸和深度。\n",
    "\n",
    "如图6.8所示，过滤器的前向传播过程就是通过左侧小矩阵中的节点计算出右侧单位矩阵中节点的过程。为了直观地解释过滤器的前向传播过程，在下面的篇幅中将给出**一个具体的样例**。在这个样例中将展示如何通过过滤器将一个2×2×3的节点矩阵变化为一个1×1×5的单位节点矩阵。一个过滤器的前向传播过程和全连接层相似，它总共需要2×2×3×5+5=65个参数，其中最后的＋5为偏置项参数的个数。假设使用 $w_{x,y,z}^i$ 来表示对于输出单位节点矩阵中的第i个节点，过滤器输入节点（x, y, z）的权重，使用 $b^i$ 表示第i个输出节点对应的偏置项参数，那么单位矩阵中的第i个节点的取值g(i)为：\n",
    "\n",
    "$$ g(i) = f(\\sum_{x=1}^2\\sum_{y=1}^2\\sum_{z=1}^3{a_{x,y,z} * w_{x,y,z}^i} + b^i)$$\n",
    "\n",
    "其中 $a_{x,y,z}$ 为过滤器中节点（x,y,z）的取值，f为激活函数。\n",
    "<p align='center'>\n",
    "    <img src=images/图6.9.JPG>\n",
    "</p>\n",
    "图6.9展示了在给定 $a, w^0$ 和 $b^0$ 的情况下，使用ReLU作为激活函数时g(0)的计算过程。在图6-9 的左侧给出了 $a$ 和 $w^0$ 的取值，这里通过3个二维矩阵来表示一个三维矩阵的取值，其中每一个二维矩阵表示三维矩阵在某一个深度上的取值。图6.9 中\"·\"符号表示点积，也就是矩阵中对应元素乘积的和。图6.9 的右侧显示了g(0)的计算过程。如果给出 $w^1$ 到 $w^4$ 和 $b^1$ 到 $b^4$，那么也可以类似地计算出g(1)到g(4)的取值。如果将 $a$ 和 $w^i$ 组织成两个向量，那么一个过滤器的计算过程完全可以通过第三章中介绍的向量乘法来完成。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**上例介绍了卷积层中计算一个过滤器的前向传播过程。卷积层的前向传播过程就是将一个过滤器从神经网络当前层的左上角移动到右下角，并且在移动中计算每一个对应的单位矩阵得到的。**\n",
    "\n",
    "具体的卷积层的前向传播过程如下图6.10所示，由于本书是黑白印刷，显示效果不够清晰，这里不采用这里的教程，建议参考[这篇blog](https://ujjwalkarn.me/2016/08/11/intuitive-explanation-convnets/)和[这个动画](https://github.com/vdumoulin/conv_arithmetic)。\n",
    "<p align='center'>\n",
    "    <img src=images/图6.10.JPG>\n",
    "</p>\n",
    "\n",
    "在这个过程中会涉及到**当前矩阵尺寸(长或宽)$in_{length}$、零填充padding值$p$、过滤器尺寸(长或宽)$f$、过滤器移动的步长stride值$s$以及得到的矩阵尺寸(长或宽)$out_{length}$这几个参数，它们的关系如下:**\n",
    "$$out_{length} = \\frac{in_{length} + 2 * p - f}{s} + 1$$\n",
    "\n",
    "**在CNN中,每一个卷积层自己内部使用的过滤器的参数是一致的，这是CNN一个非常重要的特质。直观上理解，这样共享过滤器的参数可以使得图像上的内容不受位置的影响。**参数共享的机制可以巨幅减少神经网络的参数，下面以输入层为32\\*32\\*3，目标层为28\\*28\\*16矩阵为例，来说明参数计算:\n",
    "- 全连接，则参数为$ (32*32*3+1)*28*28*16 = 38,547,712 $\n",
    "- CNN，取过滤器大小为5\\*5，步长为1，no padding，则参数为$ (5*5*3+1)*16 = 1216$\n",
    "- CNN但参数不共享，取过滤器特征同上，则不同过滤器数目为28\\*28，参数为$ (5*5*3+1)*16*28*28 = 953,344 $\n",
    "\n",
    "可以看到,CNN的稀疏连接和权值共享,二者都可以极大地减少神经网络的参数。而且**卷积层的参数个数和图片的大小无关，只和过滤器的尺寸、深度以及当前节点矩阵的深度有关**。这使得CNN可以很好地扩展到更大地图像数据上。\n",
    "\n",
    "TensorFlow对CNN提供了很好的支持，以下程序实现了一个卷积层地前向传播过程:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "'''\n",
    "# 通过tf.get_variable的方式创建过滤器的权重变量和偏置项变量。上面介绍了卷积层的参数个数只和\n",
    "# 过滤器的尺寸、深度以及当前层节点矩阵的深度有关，所以这里声明的参数变量是一个四维矩阵，\n",
    "# 前面两个维度代表了过滤器的尺寸，第三个维度表示当前的深度，第四个维度表示过滤器的深度\n",
    "filter_weights = tf.get_variable('weights', [5, 5, 3, 16], initializer=tf.truncated_normal_initializer(stddev=0.1))\n",
    "\n",
    "# 和卷积层的权重类似，当前层矩阵上不同位置的偏置项也是共享的，所以总共有下一层深度个不同偏置项，这里为16。\n",
    "biases = tf.get_variable('biases', [16], initializer=tf.constant_initializer(0.1))\n",
    "\n",
    "# tf.nn.conv2d 提供了一个非常方便的函数来实现卷积层前向传播的算法。\n",
    "# 第一个参数为当前层的节点矩阵。注意这个矩阵是一个四维矩阵，后面三个维度对应一个节点矩阵，第一维对应一个输入batch。\n",
    "#           比如在输入层，input[0, :, :, :]表示第一张图片，input[1, :, :, :]表示第二张图片，以此类推。\n",
    "# 第二个参数提供了卷积层的权重。\n",
    "# 第三个参数为不同维度上的步长。虽然第三个参数提供的是一个长度为4的数组，但是第一维和最后一维的数字要求一定是1，\n",
    "#           这是因为卷积层的步长只对矩阵的长和宽有效。\n",
    "# 最后的参数是填充（padding)的方法，TensorFlow中提供SAME或是VALID两种选择，其中VALID表示不添加，\n",
    "#           SAME表示添加0填充(TensorFlow中全0填充优先填充右下方)。\n",
    "conv = tf.nn.conv2d(input, filter_weight, strides=[1 , 1 , 1 , 1], padding＝\"SAME\")\n",
    "\n",
    "# tf.nn.bias_add可以给每一个节点加上偏置项。注意这里不能直接使用加法，因为矩阵上不同位置上的节点都需要加上同样的偏置项。\n",
    "bias = tf.nn.bias_add(conv, biases)\n",
    "\n",
    "# 将计算结果通过ReLU激活函数完成去线性化。\n",
    "actived_conv = tf.nn.relu(bias)\n",
    "'''"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 6.3.2 池化层\n",
    "在卷积层之间往往会加上一个池化层(pooling layer)。池化层可以非常有效地缩小矩阵的尺寸(池化层主要用于减小矩阵的长和宽。虽然池化层也可以减小矩阵深度，但是实践中一般不会这样使用。)，从而减少最后全连接层中的参数。**使用池化层既可以加快计算速度也有防止过拟合问题的作用。**\n",
    "\n",
    "和卷积层类似，池化层前向传播的过程也是通过移动一个类似过滤器的结构完成的。不过池化层过滤器中的计算不是节点的加权和，而是采用更加简单的最大值或者平均值运算:\n",
    "- 使用最大值操作的池化层被称之为**最大池化层(max pooling)**，这是被使用得最多的池化层结构;\n",
    "- 使用平均值操作的池化层被称之为**平均池化层(average pooling)**;\n",
    "- 其他池化层在实践中使用的比较少，本书不做过多的介绍。\n",
    "\n",
    "**与卷积层的过滤器类似，池化层的过滤器也需要人工设定过滤器的尺寸、是否使用全0填充以及过滤器移动的步长等设置，而且这些设置的意义也是一样的。**卷积层和池化层中过滤器移动的方式是相似的，唯一的区别在于卷积层使用的过滤器是横跨整个深度的(三维的)，而池化层使用的过滤器只影响一个深度上的节点(二维的)。所以池化层的过滤器除了在长和宽两个维度移动，它还需要在深度这个维度移动。下图展示了一个最大池化层前向传播计算过程。\n",
    "<p align='center'>\n",
    "    <img src=images/图6.14.JPG>\n",
    "</p>\n",
    "\n",
    "在图6.14中，不同颜色或者不同线段（虚线或者实线）代表了不同的池化层过滤器。可以看出池化层的过滤器除了在长和宽的维度上移动，它还需要在深度的维度上移动。以下TensorFlow程序实现了最大池化层的前向传播算法:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "'''\n",
    "# tf.nn.max_pool实现了最大池化层的前向传播过程，它的参数和tf.nn.conv2d 函数类似。\n",
    "# ksize提供了过滤器的尺寸, strides提供了步长信息，padding提供了是否使用全0填充。\n",
    "pool= tf.nn.max pool(actived_conv, ksize=[1, 3 , 3, 1], strides=[1 , 2, 2, 1], padding=\"SAME\")\n",
    "'''"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "对比池化层和卷积层前向传播在TensorFlow中的实现，可以发现函数的参数形式是相似的。在tf.nn.max_pool函数中，\n",
    "- 首先需要传入**当前层的节点矩阵**，这个矩阵是一个四维矩阵，格式和tf.nn.conv2d函数中的第一个参数一致。\n",
    "- 第二个参数为**过滤器的尺寸**。虽然给出的是一个长度为4的一维数组，但是这个数组的第一个和最后一个数必须为1。这意味着**池化层的过滤器是不可以跨不同输入样例或者节点矩阵深度的**。在实际应用中使用得最多的池化层过滤器尺寸［1, 2, 2, 1］或者［1, 3, 3, 1 ］。\n",
    "- 第三个参数为**步长**，它和tf.nn.conv2d 函数中步长的意义是一样的，而且第一维和最后一维也只能为1。这意味着在TensorFlow中，**池化层不能减少输入样例的个数或者节点矩阵的深度。**\n",
    "- 最后一个参数指定了**填充方式**。这个参数也只有两种取值——VALID或者SAME，其中VALID表示不使用全0填充，SAME表示使用全0填充。\n",
    "\n",
    "TensorFlow还提供了tf.nn.avg_pool来实现平均池化层，其调用格式和tf.nn.max_pool函数是一致的。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**最后是对卷积层和池化层的一个简单实验**，假设我们输入矩阵\n",
    "$\n",
    "M=\\left(\\begin{array}{c}\n",
    "1&-1&0\\\\\n",
    "-1&2&1\\\\\n",
    "0&2&-2\n",
    "\\end{array}\\right)\n",
    "$，卷积过滤器$\n",
    "W=\\left(\\begin{array}{c}\n",
    "1&-1\\\\\n",
    "0&2\n",
    "\\end{array}\\right)\n",
    "$。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "convoluted_M: \n",
      " [[[[ 7.]\n",
      "   [ 1.]]\n",
      "\n",
      "  [[-1.]\n",
      "   [-1.]]]]\n",
      "pooled_M: \n",
      " [[[[ 0.25]\n",
      "   [ 0.5 ]]\n",
      "\n",
      "  [[ 1.  ]\n",
      "   [-2.  ]]]]\n"
     ]
    }
   ],
   "source": [
    "import tensorflow as tf\n",
    "import numpy as np\n",
    "\n",
    "# 定义输入矩阵\n",
    "M = np.array(\n",
    "    [[[1],[-1],[0]],\n",
    "     [[-1],[2],[1]],\n",
    "     [[0],[2],[-2]]])\n",
    "\n",
    "# 定义参数\n",
    "filter_weight = tf.get_variable('weights', [2, 2, 1, 1], initializer = tf.constant_initializer([[1, -1],[0, 2]]))\n",
    "biases = tf.get_variable('biases', [1], initializer = tf.constant_initializer(1))\n",
    "\n",
    "# 调整输入的格式符合TensorFlow的要求。\n",
    "M = np.asarray(M, dtype='float32')\n",
    "M = M.reshape(1, 3, 3, 1)\n",
    "\n",
    "# 计算矩阵通过卷积层过滤器和池化层过滤器计算后的结果。\n",
    "x = tf.placeholder('float32', [1, None, None, 1])\n",
    "\n",
    "conv = tf.nn.conv2d(x, filter_weight, strides=[1, 2, 2, 1], padding='SAME')\n",
    "bias = tf.nn.bias_add(conv, biases)\n",
    "pool = tf.nn.avg_pool(x, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')\n",
    "\n",
    "with tf.Session() as sess:\n",
    "    tf.global_variables_initializer().run()\n",
    "    convoluted_M = sess.run(bias, feed_dict={x:M})\n",
    "    pooled_M = sess.run(pool, feed_dict={x:M})\n",
    "    \n",
    "    print(\"convoluted_M: \\n\", convoluted_M)\n",
    "    print(\"pooled_M: \\n\", pooled_M)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 1
}
